This chapter is taken with permission from the upcoming book, Advanced ColdFusion 4 Development, by Ben Forta. ISBN: 0789718103. The Chapter is authored by Nate Weiss (nweiss@icesinc.com). For more information, visit Amazon.com:
http://www.amazon.com/exec/obidos/ASIN/0789718103/002-2492923-4947031
When developing web applications with ColdFusion, you are often dealing with "complex" chunks of datañlike recordsets returned from database queries, or arrays that you fill with various pieces of information. Often, you need to get that data from one place to another. Sometimes, you even need to "pass" these chunks of data between environmentsñlike to get information from ColdFusion to Javascript, or from a COM-enabled environment like Visual Basic to ColdFusion.
At the same time, your web applications generally need to be able to "talk" in terms of plain text. For instance, it's pretty likely that you might need to pass data between two computers over the Internet. Various strategies for getting the data from place to place might already be springing into your mind. For instance, you might start thinking about passing the data around in email messages, or making it available on special "back end" web pages that another system could collect the data from. Since email and web pages are basically plain-text mediums, you need some way to turn your recordsets, arrays, and structures into blocks of ordinary ASCII text and back again.
The Web Distributed Data Exchange (WDDX) technologyñwas created to address these kinds of challenges. The idea is to be able to any kind of datañwhether it be a single number or a complex structure of arrays within other arraysñand turn it into a chunk of text. That chunk of text is kept in the WDDX "format" and can passed around from place to place with reckless abandon. When it's time to actually use the data again, the data can be read from the WDDX format back to the way it was before the whole process began.
Perhaps the coolest thing about exchanging data with WDDX is that it takes care of preserving datatypes for you. So if part of the data started out as a date variable on the way into the WDDX format, it will end up as a date variable when it comes back out. This holds true even if the data gets passed between two different programming environments.
For instance, consider a ColdFusion array that holds a number, a date, and an ordinary string. This array can be converted to WDDX and provided to a Javascript routine on a web browser. The Javascript routine will be able to refer to the array just like any other Javascript-style array, and the date stored in the array will be a true Javascript Date object, and the number will be a true Javascript Number object. The array has been successfully "passed" from CFML to an entirely different kind of language (Javascript).
Of course, it's possible to do this yourself, by writing your own ColdFusion code that dynamically creates the appropriate Javascript code. But it would be pretty complicated, especially when you start thinking about the work it would take to preserve datatypes, like dates and boolean values. By using WDDX, it's all done for you, nearly transparently.
Since WDDX is a new technology, it introduces some new concepts and new terminology. As you read this chapter and start working with WDDX, you'll come across some words that you may not have seen used in this kind of context before. Here are the basic terms and ideas to keep in mind as you start thinking about WDDX.
A WDDX Packet is any chunk of data that has been stored in the WDDX format. As you will soon see, a WDDX Packet looks somewhat like HTML or CFML. Since it's tag-based like HTML, a WDDX packet is simple, easy to read, and practically describes itself. Each individual piece of information is surrounded by opening and closing tags (similar to opening and closing BODY
or TITLE
tags in an HTML document), which allows complex datatypes to be stored in the packet. That's something which is pretty hard to accomplish with other text-based formatsñlike comma-separated or space-delimited text.
Serializing is the process of taking some piece of data and converting it into a WDDX Packet. To serialize data, you need to be using a language or environment that has access to some kind of function or procedure that knows about the WDXX format and how to serialize data properly. For instance, in a ColdFusion template, the serialization process is performed by the CFWDDX
tag. In other languages and environments, the functions or methods that you use to serialize a particular chunk of data will be different, but the resulting WDDX Packet should be the same, and will be able to be understood by any program that supports WDDX properly.
As a quick example, serializing the string "Hello, World!" would create a WDDX Packet that includes the string itself, surrounded by a pair of string
tags, like this:
<string>Hello, World!</string>
As you can see, this allows the WDDX Packet to hold descriptions of your data right along with the data itself. That's one of the neatest things about the WDDX formatñthe packet is able to describe itself to whatever application needs to read it in.
Deserializing is the opposite of serializing. It's the process of taking an existing WDDX Packet and pulling the actual data out of it. For instance, if an application needed to "unpack" the "Hello, World" snippet shown above, it would first look at the tags to learn what kind of data is in the packet. Since WDDX is saying that this packet contains string data, the application knows that it should store the value sitting between the tags as a string. For "typed" development environments, such as Visual Basic, C++, or Delphi, the datatype is often very important.
WDDX support is available for a number of languages and development tools. Of course, it's supported by CFML, which just means that Allaire has built WDDX right into ColdFusion Application Server. In classic Allaire style, all of the WDDX functionality is exposed to us ColdFusion developers as a single, very easy-to-use package: the CFWDDX
tag. You'll find the CFWDDX
tag in nearly all of the code listings in this chapter.
Allaire also provides WDDX support for Javascript, which allows you to make ColdFusion variables "visible" to Javascript. This allows you to make your pages and forms "dynamic" with much less scripting than ever beforeñwithout locking you into proprietary code from any one browser vendor.
Allaire also provides a COM object that brings WDDX functionality to any COM-enabled development tool or application. This means that you can use WDDX to share information between ColdFusion, Active Server Pages (ASP), Visual Basic, and applications built with COM-enabled development tools like Visual Basic, Delphi, Visual C++, or PowerBuilder. This COM object is capable of doing all the WDDX-related things that ColdFusion templates can do natively.
This chapter covers using WDDX with ColdFusion and Javascript. The next chapter, "Advanced WDDX Integration", discusses how to use WDDX with Allaire's COM object. Examples are provided for Active Server Pages and Visual Basic. It also discusses how to use WDDX with PERL.
You've already learned that a simple string value will be placed between <string>
tags. The WDDX specification says that a few additional tags must appear at the beginning and end of every WDDX Packet in order for it to be considered "valid" or "well-formed".
The WDDX Packet shown in Listing 1 shows a complete WDDX packet that contains the "Hello World" string value that was discussed above. The entire packet is enclosed between a pair of <wddxPacket>
tags. As of this writing, the version attribute will always be 0.9. If the WDDX specification changes at some point in the future, the version number will be updated accordingly. This way, before an application attempts to deserialize a packet, it can check the version numberñto make sure that it will know how to understand all of the tags in the packet before it actually starts.
Next, a pair of <header>
tags appears. The <header>
tags don't really serve any purpose in WDDX at this time, but may come to hold significant information in a future version of WDDX. After the <header>
tags, a pair of <data>
tags appears, and all of the tags that contain the actual serialized information are placed between them. For instance, in Listing 1, a single pair of <string>
tags are placed between the <data>
tags. If the data in the packet was a date value instead of a string, a pair of <dateTime>
tags would appear there instead.
Listing 1 - StringPacket.txt - A very simple WDDX packet
<wddxPacket version='0.9'> <header></header> <data> <string>Hello, World!</string> </data> </wddxPacket>
There are more complicated examples of WDDX Packets later on in this chapter. For instance, to see how a ColdFusion "structure" looks like when serialized into a WDDX Packet, skip ahead to Listing 6. To see what a query recordset looks like after being converted to WDDX, skip ahead to Listing 10. As you can see, the <wddxPacket>
, <header>
, and <data>
parts always remain the same.
Note
Whitespace is ignored between the pairs of tags in a WDDX Packet. What this means is that the blank lines, hard returns, and indenting that appear in Listing 1 can be removed without affecting anything. For instance, Figure 1 shows the same WDDX Packet without any added whitespace. In general, the packets produced by ColdFusion and other systems will not have any whitespace added. But you can add whitespace if you wish, to make the packet more readable.
As you'll see in this chapter and the next chapter, WDDX can do a lot for you. As an added bonus, it just so happens that the WDDX format is compliant with the Extensible Markup Language (XML) specification, which means that the packet shown aboveñand every WDDX Packet generated by your applicationsñwill be "well-formed" XML.
Of course, it's nice that WDDX was conceived as an XML-compliant technologyñopen standards are a good thing. But it doesn't really have much of an impact on your day-to-day life as a ColdFusion developer, because Allaire's support for WDDX successfully hides all the details about XML parsing from you. In particular, you don't have to worry about "real" XML concepts like entities, attributes, and DTDs. In other words, the fact that WDDX is actually XML "under the hood" is so transparent that it hardly even matters.
On The CD
If you're a stickler for XML technicalities and want to look at the DTD (Document Type Definition) for the WDDX format, it's included on the CD for this chapter. The filename is wddx_0090.dtd, and should be understood by any XML editor or validation tool.
As you've already learned, WDDX is not something that exists only for ColdFusion. It can also be used to pass information between other types of applicationsñin fact, that may be the most interesting thing about it. But because you're already developing ColdFusion applications, and because there are a number of ways that WDDX can be helpful in ColdFusion-only situations, it makes sense to learn about using WDDX in ColdFusion first. In this section, you'll see what an actual WDDX Packet looks like and how to serialize and deserialize the Packet in a ColdFusion template.
Each of the environments that support WDDX will have some way to serialize and deserialize WDDX Packets. In a ColdFusion template, you use the CFWDDX
tag to serialize and serialize data from native ColdFusion variables to the WDDX Packet format. You can also use the CFWDDX
tag to deserialize the data from the WDDX Packet format back into native ColdFusion variables.
CFWDDX
tag makes it very easy to serialize a ColdFusion variable into the WDDX format. For instance, take a look at Listing 1. This template defines a ColdFusion variable called Message
which holds a simple string value. It then passes the variable to a CFWDDX
tag, as the tag's INPUT
parameter.
The most important item in this template is the CFWDDX
tag's ACTION
parameter, which has been set to CFML2WDDX
. This tells the tag that we are interested in taking a value from CFML and converting it to WDDX (the "2" stands for "to"). After the CFWDDX tag serializes the message, it stores the resulting WDDX Packet in the variable specified in the OUTPUT parameter of the tag. So in this template, the converted message gets stored in a variable called MyWDDXPacket
.
The template then outputs the packet to the browser so we can see what the serialized version of the message looks like. Figure 1 shows the results.
Listing 2 - Serialize.cfm - Serializing a simple string into the WDDX format
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>Message Serializer</title> </head> <body> <H2>Message Serializer</H2> <!--- Set the #Message# variable to a simple string value ---> <CFSET Message = "Hello, World!"> <!--- Serialize the #Message# variable into a WDDX Packet ---> <CFWDDX INPUT="#Message#" OUTPUT="MyWDDXPacket" ACTION="CFML2WDDX"> <!--- Output WDDX Packet so we can see what it looks like ---> <!--- (HTMLEditFormat function lets us see tags properly) ---> <CFOUTPUT> <P><B>Original Message:</B> #Message#</P> <P><B>The message was serialized into the following WDDX Packet:</B></P> #HTMLEditFormat(MyWDDXPacket)# </CFOUTPUT> </body> </html>
Note
Because the MyWDDXPacket
variable contains tags that look like HTML tags, a web browser will not display the packet's contents unless each <
and >
sign is converted to a <
or >
symbol. ColdFusion's HTMLEditFormat
function escapes these kinds of special characters automatically, which is why it's used in Listing 1. You could leave out the HTMLEditFormat
function if you wanted to, but you would need to use the browser's "View Source" command to actually see the packet's contents.
Tip
If you wanted, you could use ColdFusion's HTMLCodeFormat
function instead of the HTMLEditFormat
function to output the MyWDDXPacket
variable. That would cause the browser to display the packet's contents using a fixed-width font, all on one long line.
Figure 1 - Simple strings get placed between <string> tags in the WDDX format
It's just as easy to do the WDDX conversion in the opposite directionñthat is, to take an existing WDDX Packet and "parse" it into the appropriate ColdFusion variable. For instance, a template could take a WDDX Packet posted by a form, deserialize it, and place the deserialized value into a variable. The variable should then contain the original message string ("Hello, World!"), and should be able to be used in the rest of the template just like any other variable.
To demonstrate this idea, the template from Listing 1 can be modified so that it includes a simple form that posts the WDDX Packet to another template. All that's needed in the form is a single hidden field to hold the packet itself, and a submit button. Listing 2 shows the code to produce this simple form, which is shown in Figure 2.
Listing 3 - Serialize2.cfm - Posting a WDDX Packet to another ColdFusion template
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>Message Serializer</title> </head> <body> <H2>Message Serializer</H2> <!--- Set the #Message# variable to a simple string value ---> <CFSET Message = "Hello, World!"> <!--- Serialize the #Message# variable into a WDDX Packet ---> <CFWDDX INPUT="#Message#" OUTPUT="MyWDDXPacket" ACTION="CFML2WDDX"> <!--- Output WDDX Packet so we can see what it looks like ---> <!--- (HTMLEditFormat function lets us see tags properly) ---> <CFOUTPUT> <P><B>Original Message:</B> #Message#</P> <P><B>The message was serialized into the following WDDX Packet:</B></P> #HTMLEditFormat(MyWDDXPacket)# <!--- Simple form that posts WDDX Packet as hidden field ---> <FORM ACTION="Deserialize.cfm" METHOD="POST"> <INPUT TYPE="HIDDEN" NAME="WDDXContent" VALUE="#HTMLEditFormat(MyWDDXPacket)#"> <INPUT TYPE="SUBMIT" VALUE="Post WDDX Packet to Next Page"> </FORM> </CFOUTPUT> </body> </html>
Note
Since WDDX Packets will often contain double quote marks, the HTMLEditFormat
function should be used when supplying a packet's contents to the VALUE
parameter of a INPUT
or OPTION
tag on a form. For instance, if the Message
variable contained a double quote mark, the browser would think that the VALUE
parameter of the hidden field ended at that quote mark (instead of the ending quote mark that's in the .cfm template itself, after the #MyWDDXPacket#
variable). The form page would probably not display properly, and when the form was posted, the packet's contents would be "chopped off" at the embedded quote mark, making it unusable.
Figure 2 - A WDDX Packet can be "hidden" in a form and posted to another web page
The form in Listing 2 posts its data to a template called Deserialize.cfm, so putting that template together is the next task at hand. The template needs to take the posted WDDX Packetñwhich will be available in the #Form.WDDXContent#
variableñand deserialize it into a native ColdFusion variable.
The code in Listing 3 does just that. Just like the serialization templates you've seen so far, this template uses the CFWDDX
tag to do all the WDDX conversion work. Because it is interested in getting data from WDDX to CFML, this template uses WDDX2CFML
for the tag's ACTION
parameter (instead of CFML2WDDX
). The #Form.WDDXContent#
variableñwhich holds the WDDX Packet from the preceding formñis fed to the tag's INPUT
parameter.
Working with the data extracted from the packet is simple and straightforward. The CFWDDX
tag's OUTPUT
parameter tells ColdFusion to extract the actual value from the packet and place it into the #PostedMessage#
variable. You are then free to work with the variable just as you would work with any other variable. This simple example just displays the variable's value on the current web page to prove that the packet was deserialized successfully. Figure 3 shows the results.
Listing 4 - Deserializing a WDDX Packet back into a normal ColdFusion variable
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>Message De-serializer</title> </head> <body> <H2>Message De-serializer</H2> <!--- Deserialize the WDDX Packet posted by form field ---> <CFWDDX INPUT="#Form.WDDXContent#" OUTPUT="PostedMessage" ACTION="WDDX2CFML"> <!--- Output WDDX Packet so we can see what it looks like ---> <!--- (HTMLEditFormat function lets us see tags properly) ---> <CFOUTPUT> <P><B>Posted Message:</B> #PostedMessage#</P> </CFOUTPUT> </body> </html>
Figure 3 - It's easy to deserialize an incoming WDDX Packet from another page
So you've seen that a string can be serialized, passed to another page, and then deserialized back to its original form. While the process appears to work just fine, you may be thinking that it's not exactly ground-breaking. For instance, why not just pass the #Messsage#
variable itself in the form's hidden field, and skip all this serialization / deserialization business altogether?
Well, things get a whole lot more interesting when you consider that the INPUT
parameter of our serialization template (Listing 2) can accept any ColdFusion variable, not just a simple string value. It could be an array, a structure, an array within a structure, and so on.
Tip
The code in this section makes use of ColdFusion Structures, which are new for ColdFusion 4.0. Structures are similar to arrays but are indexed by "key" rather than by number. Structures are basically the same as what other languages call "associative arrays." See the Working with Structures chapter in your ColdFusion documentation if you want to learn more about Structures.
Suppose that you need to build a "book finder" web page that would submit a request for information from another server or application. The request will include a few parameters about the books for which information is neededñlike Author names, number of pages, and publication date. These parameters need to be submitted with the request, and the other server or application will presumably respond by sending back information about books that match those parameters.
A good way to get those parameters to the other application would be to create a ColdFusion Structure that holds each of the parameters. Then you need only modify the code in Listing 2 so that it serializes the Structure instead of the simple #Message#
variable that we've been serializing up to now.
The code in Listing 4 does just that. It creates a Structure called #Request# which holds a date value, a numeric value, and an array of author names. The whole structure is serialized into a WDDX Packet, and the packet is placed into a hidden field just as in Listing 2. Figure 4 shows the results.
Listing 5 - Serialize3.cfm - Serializing complex datatypes requires no additional work
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>Remote Book Finder</title> </head> <body> <H2>Remote Book Finder</H2> <!--- Create an array of Author names ---> <CFSET Authors = ArrayNew(2)> <CFSET Authors[1][1] = "Ben"> <CFSET Authors[1][2] = "Forta"> <CFSET Authors[2][1] = "Douglas"> <CFSET Authors[2][2] = "Adams"> <!--- #Request# structure to hold the array and other info ---> <CFSET Request = StructNew()> <CFSET Request["Author"] = Authors> <CFSET Request["PubDateFrom"] = ParseDateTime("3/18/1997")> <CFSET Request["PagesAtLeast"] = Val(200)> <!--- Serialize the #Request# variable into a WDDX Packet ---> <CFWDDX INPUT="#Request#" OUTPUT="MyWDDXPacket" ACTION="CFML2WDDX"> <!--- Output WDDX Packet so we can see what it looks like ---> <!--- (HTMLEditFormat function lets us see tags properly) ---> <CFOUTPUT> <P><B>The structure was serialized into the following WDDX Packet:</B></P> #HTMLEditFormat(MyWDDXPacket)# <!--- Simple form that posts WDDX Packet as hidden field ---> <FORM ACTION="Deserialize3.cfm" METHOD="POST"> <INPUT TYPE="HIDDEN" NAME="WDDXContent" VALUE="#HTMLEditFormat(MyWDDXPacket)#"> <INPUT TYPE="SUBMIT" VALUE="Post WDDX Packet to Next Page"> </FORM> </CFOUTPUT> </body> </html>
Tip
If you need to ensure that ColdFusion knows to serialize a particular value as a number (rather than as a string), use the Val
function when setting the variableñas Listing 4 does for the PagesAtLeaset element. Otherwise, ColdFusion's "typeless" nature will cause it to put <string>
tags instead of <number>
tags around the value. That could be a problem if you were passing the serialized packet to an application that isn't as forgiving about datatypes.
Tip
Similarly, if you need to ensure that a date gets serialized as a dateTime "object" in the WDDX Packet (rather than as a simple string value), use the ParseDateTime
function as shown in Listing 4. Alternatively, you could use the CreateDate
function (handy if you have the year, month, and day as separate values). Either function returns a ColdFusion date/time value, which CFWDDX
will serialize properly to WDDX's dateTime datatype.
Figure 4 - Structures and arrays can also be serialized and deserialized with ease
It's worth taking a moment to examine the structure of the WDDX Packet shown in Figure 4. It looks a bit cryptic when the tags are all mashed together, but the format of the packet is really quite simple. If some indenting is added to the packet to make it easier to read, it becomes clear how sensibly WDDX deals with structures and arrays. Listing 5 shows what you might end up with if you copied and pasted the packet into a text editor and added some indenting.
Listing 6 - StructPacket.txt - The WDDX Packet shown in Figure 4, indented for clarity
<wddxPacket version='0.9'> <header></header> <data> <struct> <var name='PAGESATLEAST'> <number>200</number> </var> <var name='PUBDATEFROM'> <dateTime>1997-3-18T0:0:0-4:0</dateTime> </var> <var name='AUTHOR'> <array length='2'> <array length='2'> <string>Ben</string> <string>Forta</string> </array> <array length='2'> <string>Douglas</string> <string>Adams</string> </array> </array> </var> </struct> </data> </wddxPacket>
You already know that the <wddxPacket>
, <header>
, and <data>
tags are always the same, so just concentrate on the stuff between the <data>
tags. Next in the chain is the pair of <struct>
tags that encloses the entire Structure. Then each "element" of the structure is described by a pair of <var>
tags that specifies the element's "key" in its name
attribute. Then, inside the <var>
tags, the simple values (the number and the date) can be described by enclosing the value within <number>
or <dateTime>
tags.
Since the third element in the structure is an array, its individual values (the two authors) are surrounded by two levels of <array>
tags. The outer level indicates the first "dimension" of the two-dimensional array (a two-dimensional array is technically an array of arrays). The inner level represents the second dimension of the arrayñwhich is made up of one array for first names and a second array for last names. Inside the <array>
tags, the actual data is treated in the same way as the simple number and date values were. Pretty simple stuff, really, but well-thought out and quietly powerful.
Tip
Even though indentation and blank lines have been added to Listing 5, it's still a perfectly valid WDDX Packet. It will be deserialized in exactly the same way as the packet shown in Figure 4 will be. So if for some reason you want to make your application WDDX packets more readable for human beings by adding lines, spaces, and tabs between the tags, go right ahead. All that extra "whitespace" will be ignored by the WDDX deserialization process.
To receive and work with the "request" packet shown in Listing 5, the deserialization code from Listing 3 needs to be adapted only slightly to prove that the structure can be successfully extracted from the WDDX Packet without losing anything. Actually, nothing about the CFWDDX
tag needs to change, which is one of the great things about WDDX. The conversion processñin both directionsñis completely transparent to us as ColdFusion developers. Complex datatypes and simple datatypes are dealt with in exactly the same way.
The only thing that needs to be changed is the code to display the data. This is code that would have needed to be written anyway to get the data displayed on the browser. Listing 6 provides code that will deserialize the structure, display its simple number and dateTime elements, and iterate through the array of authors, displaying the value of each item in the array. Figure 5 shows the results.
Listing 7 - Deserialize3.cfm - Deserializing the structure and the embedded array
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>Request De-serializer</title> </head> <body> <H2>Request De-serializer</H2> <!--- Deserialize the WDDX Packet posted by form field ---> <!--- Original structure values now in #PostedRequest# ---> <CFWDDX INPUT="#Form.WDDXContent#" OUTPUT="PostedRequest" ACTION="WDDX2CFML"> <!--- Display info in structure (from the WDDX packet) ---> <CFOUTPUT> <!--- Output simple-value elements from the structure ---> <P><B>Only books over:</B><BR> #PostedRequest["PagesAtLeast"]# pages <P><B>Published After:</B><BR> #DateFormat(PostedRequest["PubDateFrom"], "mmmm d, yyyy")# <!--- We know "Author" element of structure is an array ---> <!--- Set it to a local array variable for simplicitly ---> <CFSET PostedAuthors = PostedRequest["Author"]> <!--- Loop thru PostedAuthors array to output each name ---> <P><B>Authors:</B><BR> <CFLOOP FROM="1" TO="#ArrayLen(PostedAuthors)#" INDEX="Counter"> Author #Counter#: #PostedAuthors[Counter][2]#, <!--- Last Name ---> #PostedAuthors[Counter][1]#<BR> <!--- First Name ---> </CFLOOP> </CFOUTPUT> </body> </html>
Figure 5 - The WDDX deserialization process preserves complex datatypes, too!
This is WDDX's real strengthñthat it can exchange complex datatypes such as structures and arrays without a problem. As you'll see in the next section, it can also be used to exchange entire query-style recordsets, like the results returned by a CFQUERY
tag.
If you've done any work with ColdFusion's CFHTTP
tag, you know that it can be used to "fetch" web pages from any webserver on the Internet. Basically, the CFHTTP
tag "pretends" to be a web browser, supplying any parameters that would normally be supplied by form input, cookies, or CGI variables. ColdFusion developers already use this tag to have their applications automatically "visit" other web pages programmatically.
For instance, a ColdFusion application might need to know the current temperature. By using CFHTTP
to fetch a page that includes the current temperatureñperhaps the "current conditions" page of the local airport's websiteñthe application can obtain a document that has the needed information in it. Then, using ColdFusion's string manipulation functions, or some regular expressions, the application can parse through the page's source code and extract the few characters that represent the temperature.
Note
Refer to Chapter ??, "Chapter Title Here", for a complete discussion of how the CFHTTP
tag can be used to fetch web pages from other web servers on the Internet, or from servers on your Intranet or Extranet.
Okay, now imagine taking things a step further. What if the airport's website had a special page that wasn't meant to be looked at, but rather was meant only to supply information to other systems? That is, instead of including pictures, links, table and font tags, explanatory text, and so on, what if all the page contained was a WDDX Packet that contained the temperature? Maybe the packet includes other information as well, like the barometric pressure, runway conditions, and so on.
That would mean that any ColdFusion application would be able to use the CFHTTP
tag to "pick up" this packet, then use CFWDDX
to extract all the information from the packet into local variables. Just two lines of CFML code later, the application has the information it needs. It's easy to imagine that other airports around the world might set up the same kind of "back-end" web pages to report the current conditions. The airports might even use these pages to get information about each other's current conditions, to be able to tell customers what the weather is like at their destinations.
Suddenly, the airport's website is no longer just supplying information to people who happen to visit the website and click on the "current conditions" page. It's now a part of an ambitious information and automation network. No expensive communications channels were set up, and no complicated "integration" work was done. By using the infrastructures already in placeñnamely, the airport's webserver and Internet connectionñthe airport was able to transform itself into a source of raw data for any application that knows how to fetch a web page and how to deserialize a WDDX Packet.
Note
Of course, ColdFusion knows how to do both of these things, and fetching the WDDX Packet and deserializing it with ColdFusion are what you'll learn to do next. But as you'll see later in this chapter, so PERL and Javascript can also be used to fetch and deserialize the information. And so can any COM-enabled application, like Visual Basic, Delphi, or Visual C++.
The Deserialize3.cfm template shown in Listing 6 demonstrated how a ColdFusion structure could be easily collected from form input and deserialized, making the structure's various elements available for display. Now, instead of just displaying the information from the structure, let's turn the code from Listing 6 into a kind of automated "robot" that waits for incoming requests and responds to them.
Take a look at the BookRobot.cfm template shown in Listing 7. This robot will run a database query based on the parameters contained within the incoming Request
structure and send back the query results as a WDDX Packet. Don't worryñthe robot template may appear to be complicated at first glance, but as soon as you take a closer look, you'll see that it's actually very simple.
There are really only three ColdFusion tags of any consequence here, each taking care of a specific logical step. The first CFWDDX
tagñunchanged from Listing 6ñdeserializes the incoming WDDX Packet and places it in the Request
structure. Next, the CFQUERY
tag builds a database query "on-the-fly", based on the various elements contained within the structure. Finally, the second CFWDDX
tag converts the entire recordset returned by the CFQUERY
into a WDDX Packet, outputting it to the current page.
Listing 8 - BookRobot.cfm - A "Book Robot" that answers queries via WDDX
<!--- Deserialize the WDDX Packet posted by form field ---> <!--- Original structure values now in #PostedRequest# ---> <CFWDDX INPUT="#WDDXContent#" OUTPUT="PostedRequest" ACTION="WDDX2CFML"> <!--- The PostedRequest Structure may contain these elements: "Columns" - the database columns to fetch from table "ISBN" - the ISBN number for a specific book "PagesAtLeast" - only books with this many pages or more "PubDateFrom" - only books published on/after this date "Author" - a 2D array of author first/last names "Sort" - order in which to return the results ---> <!--- Fetch the information from the database ---> <CFQUERY NAME="GetBooks" DATASOURCE="A2Z"> <!--- If request structure has a "Columns" element, ---> <!--- SELECT those. Otherwise default to ISBN/Title ---> <CFIF StructKeyExists(PostedRequest, "Columns")> SELECT #PostedRequest["Columns"]# <CFELSE> SELECT ISBN, Title </CFIF> FROM Inventory WHERE 0=0 <!--- If "ISBN" element in request structure ---> <CFIF StructKeyExists(PostedRequest, "ISBN")> AND ISBN = '#PostedRequest["ISBN"]#' </CFIF> <!--- If "PagesAtLeast" element in request structure ---> <CFIF StructKeyExists(PostedRequest, "PagesAtLeast")> AND Pages > #PostedRequest["PagesAtLeast"]# </CFIF> <!--- If "PubDateFrom" element in request structure ---> <CFIF StructKeyExists(PostedRequest, "PubDateFrom")> AND PublicationDate >= #CreateODBCDate(PostedRequest["PubDateFrom"])# </CFIF> <!--- If "Author" element (array) in request structure ---> <CFIF StructKeyExists(PostedRequest, "Author")> <!--- Set local array variable for simplicity ---> <CFSET PostedAuthors = PostedRequest["Author"]> AND (0=1 <!--- Loop through each author in the array ---> <CFLOOP FROM="1" TO="#ArrayLen(PostedAuthors)#" INDEX="ThisAuth"> OR (AuthorFirstName = '#PostedAuthors[ThisAuth][1]#' AND AuthorLastName = '#PostedAuthors[ThisAuth][2]#') </CFLOOP> ) </CFIF> <!--- If request structure has a "Sort" element, ---> <!--- ORDER BY that. Otherwise default to Title ---> <CFIF StructKeyExists(PostedRequest, "Sort")> ORDER BY #PostedRequest["Sort"]# <CFELSE> ORDER BY Title </CFIF> </CFQUERY> <!--- Convert the query results (recordset) to WDDX ---> <CFWDDX INPUT="#GetBooks#" ACTION="CFML2WDDX">
It's the second part of the templateñthe dynamically-created query statementñthat makes the template look complicated. If you imagine that the "Author" part of the CFQUERY
code was taken out, for instance, or handled in a simpler manner, the template would really be short and sweet. Considering what it doesñyou'll see it in action in a momentñits brevity and simplicity is pretty amazing.
Tip
When you want a web page to only return a WDDX Packet, remember not to include the various HTML tags that you would normally include in a ColdFusion template or static web page. For instance, there are no HEAD
, TITLE
, or BODY
tags in Listing 7, since it is not intended to ever be "rendered" visually by a web browser.
Note
The second CFWDDX
tag shown above does not have an OUTPUT
parameter. This causes the tag to simply output the WDDX Packet it creates onto the current page, instead of placing the packet's contents into a variable. If you wanted to, you could add an OUTPUT
parameter that specifies a variable name, then output the variable's contents by placing it between a pair of CFOUTPUT
tags (after the CFWDDX
tag). The results would be exactly the same. Omitting the OUTPUT
parameter is just a shortcut.
Since the robot is going to respond to queries with a WDDX Packet rather than by displaying a normal web page, it's not really designed to be accessed by humans. But it can be tested with an ordinary HTML form and a web browser, to make sure that the robot actually works correctly.
The simple "tester" code shown in Listing 8 can be used to test the robot. It just creates a simple form that a WDDX Packet can be pasted into. When the form is submitted, the robot will examine the packet, run the appropriate database query, and return the results as a new WDDX Packet.
Listing 9 - BookRobotTester.cfm - A simple form that invokes the Book Robot
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>Robot Tester</title> </head> <body> <FORM ACTION="BookRobot.cfm" METHOD="POST"> To test the robot, paste a WDDX Packet in here and submit:<BR> <TEXTAREA NAME="WDDXContent" ROWS="10" COLS="60" WRAP="VIRTUAL"></TEXTAREA> <P><INPUT TYPE="Submit"> </FORM> </body> </html>
Figure 6 - The Book Robot can be tested with an ordinary HTML form
You already have a WDDX Packet that you can use with the testerñthe packet from Listing 5. Just bring the Serialize3.cfm template up in your browser (see Figure 4), then cut-and-paste the packet into the tester form's textarea and submit. Figure 6 shows what the form looks like after the packet has been pasted into it.
The robot's response to the form submission won't look like much. Depending on the browser, you may see the some book titles run together into one long paragraph, or you may just get a blank page. But if you use your browser's "View Source" command, you'll see that the robot has indeed responded with a WDDX Packet that contains the ISBN number and Title for each book that matched the parameters from the test packet you submitted.
Listing 9 shows the WDDX packet returned by the robot, with extra lines and indenting added. This is the first time you're seeing how WDDX serializes a recordset. As you can see, it's very similar to the way that it treats structures (see Listing 5). First, the entire recordset is wrapped in a pair of <recordset>
tags. The opening <recordset>
tag specifies the number of rows in its rowCount
attribute, and the column names from the database in the fieldNames
attribute. Inside the recordset tags, the actual contents of each column are provided, each column enclosed in a pair of <field>
tags. Within each <field>
tag, the data from row from the column appears between a pair of <string>
, <number>
, or <dateTime>
tags, as appropriate.
Listing 10 - RecordsetPacket.txt - The robot's response to the test, with whitespace added for clarity
<wddxPacket version='0.9'> <header></header> <data> <recordset rowCount='3' fieldNames='ISBN,TITLE'> <field name='ISBN'> <string>0345391829</string> <string>0789709708</string> <string>0517545357</string> </field> <field name='TITLE'> <string>Life, the Universe and Everything</string> <string>The Cold Fusion Web Database Construction Kit</string> <string>The Restaurant at the End of the Universe</string> </field> </recordset> </data> </wddxPacket>
Now that the robot has been proven to work correctly, all that's needed is to come up with the code that will actually submit requests to the robot. Instead of using an on-screen form as the tester template does, the CFHTTP
tag will be used to "mimic" a form submission. Then the robot's responseñwhich will be structured like the packet shown above, in Listing 9ñcan be deserialized and shown to the user.
The CFFORM
-based code in Listing 10 creates a simple on-screen form that people will be able to use to get information about books in the A2Z database's Inventory table. Users won't realize it, but when they use the form, they won't be directly interacting with a copy of the database on the server. Instead, the form is posted to the BookFinder.cfm template (discussed in a moment), which will send the request on to the Book Robot. So the Book Robot will be handling the actual database queries--the template that this form posts to will merely act as a sort of "proxy" for the Book Robot.
Listing 11 - FinderForm.cfm - A search form for users to interact with the "Book Robot"
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>Book Finder</title> </head> <body> <H2>Book Finder</H2> <!--- Simple form where the user can enter parameters ---> <CFFORM ACTION="BookFinder.cfm" METHOD="POST"> <!--- Blank to enter publication date "filter" ---> <P><B>Only books published after:</B><BR> <CFINPUT NAME="PubDateFrom" SIZE="10" VALIDATE="DATE"><BR> <!--- Blank to enter min. number of pages ---> <P><B>Only books with at least:</B><BR> <CFINPUT NAME="PagesAtLeast" SIZE=5 VALIDATE="integer"> pages<BR> <!--- Drop-down menu to choose RobotServer ---> <P><B>Location:</B><BR> <SELECT NAME="RobotServer"> <OPTION VALUE="http://127.0.0.1">This store <OPTION VALUE="http://nyc.a2zbooks.com">New York store <OPTION VALUE="http://tokyo.a2zbooks.com">Tokyo store </SELECT><BR> <!--- Blanks to enter up to 5 author names ---> <P><B>Authors:</B> (LastName, FirstName)<BR> <CFLOOP FROM="1" TO="5" INDEX="Counter"> <CFOUTPUT> <INPUT NAME="LastName#Counter#">, <INPUT NAME="FirstName#Counter#"><BR> </CFOUTPUT> </CFLOOP> <!--- Submit button ---> <HR><CENTER><INPUT TYPE="Submit" VALUE="Find Books Now"></CENTER> </CFFORM> </body> </html>
Figure 7 - Users could query a Book Robot operating at any store location worldwide
Now take a look at Listing 11, which receives the input posted by the form in Listing 10, serializes the form input into a WDDX Packet, and posts the packet to the Book Robot template. Again, this is a template that looks a bit more complicated than it actually is. If you look at its component parts, you can see that it is made up of just a few ColdFusion tags, each of which representing a discreet logical step. As you look it over, note that except for the CFHTTP part at the end, the basic idea of this template is very similar to the code found in Listing 4.
Tip
This template makes use of ColdFusion scripting, which is new for ColdFusion 4.0. If the syntax in the CFSCRIPT block looks unfamiliar to youñtake a quick look back at Chapter ???, "ColdFusion Scripting", for a discussion about the if
, for
, and assignment statements used in this template.
Listing 12 - BookFinder.cfm - Interacting with the Book Robot via CFHTTP
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>Book Finder</title> </head> <body> <H2>Book Finder - Results</H2> <!--- Assemble the user's input into structure ---> <CFSCRIPT> // New request structure to pass to robot Request = StructNew(); // If user entered a date, include in structure if (IsDate(Form.PubDateFrom)) { Request["PubDateFrom"] = Form.PubDateFrom; } // If user entered minimum number of pages if (IsNumeric(Form.PagesAtLeast)) { Request["PagesAtLeast"] = Form.PagesAtLeast; } // Create 2D array for Authors entered on form Authors = ArrayNew(2); // For each of the 5 blanks, check if name was entered for (Counter = 1; Counter LT 5; Counter = Counter + 1) { First = Evaluate("Form.FirstName#Counter#"); Last = Evaluate("Form.LastName#Counter#"); // if a name was entered, add to Authors array if ( (First is not "") AND (Last is not "") ) { Authors[Counter][1] = First; Authors[Counter][2] = Last; } } // if names in Authors array, add to Request structure if (ArrayLen(Authors) GT 0) { Request["Author"] = Authors; } </CFSCRIPT> <!--- Convert the structure to WDDX Packet ---> <CFWDDX INPUT="#Request#" OUTPUT="RequestAsWDDX" ACTION="CFML2WDDX"> <!--- Post the WDDX Packet to the "robot" page as a form field ---> <CFHTTP URL="#Form.RobotServer#/Robots/BookRobot.cfm" METHOD="POST"> <CFHTTPPARAM NAME="WDDXContent" VALUE="#RequestAsWDDX#" TYPE="FORMFIELD"> </CFHTTP> <!--- Extract recordset from WDDX Packet in robot's response ---> <CFWDDX INPUT="#CFHTTP.FileContent#" OUTPUT="FetchedBooks" ACTION="WDDX2CFML"> <!--- Output the "fetched" books to browser ---> <UL> <CFOUTPUT QUERY="FetchedBooks"> <LI>#Title# </CFOUTPUT> </UL> </body> </html>
The first thing the template in Listing 11 needs to do is to put together a request structure to pass to the Book Robot. This takes place in the CFSCRIPT
block at the top of the template. It looks long and complicated, but it's really notñall it does is check to see if the user entered anything in each of the form fields, and if so, adds a corresponding element to the request structure. Next, the structure is serialized into a WDDX Packet called WDDXContent
, which will look more or less like the WDDX Packet generated by Listing 4 and shown in Listing 5.
Then the template uses the CFHTTP
tag to "post" the request to the Book Robot template. The robot processes the request and responds, just as it did when the request was coming from the test form (Figure 6). ColdFusion stores the robot's response in the #CFHTTP.FileContent#
variable, which should contain a WDDX Packet that looks a lot like Listing 9. That variable is fed to the second CFWDDX tag, which deserializes the robot's response back into a recordset called FetchedBooks
.
Finally, the template can display the title of each book returned by the Robot by referring to the #Title#
variable inside of a CFOUTPUT
block that specifies FetchedBooks
as its QUERY
parameter. In other words, the FetchedBooks query recordset can be used just as if the CFQUERY
had taken place earlier in the same template.
Note
The URL
parameter of the CFHTTP
tag is looking for the BookRobot.cfm template to be located in a directory called Robots. In other words, after the #Form.RobotServer#
variable is evaluated, ColdFusion will go to http://127.0.0.1/Robots/RobotServer.cfm to post the request. So, if you are trying to follow along with these examples, you will need to configure your webserver to have a "virtual directory" that maps /Robots
to the folder where the RobotServer.cfm file actually is.
At this point, the whole thing should actually work! Type some search criteria into the form and submit it. The BookFinder.cfm template will receive the form input, interact with the Book Robot, and display the titles of the matching books. Figure 8 shows the results.
Figure 8 - The Book Robot's response is deserialized and displayed to the user
The bulk of the code in Listing 11ñthat is, the parts that put together the request, interact with the robot, and deserialize the robot's responseñmight need to be re-used in other templates. Therefore, it makes a lot of sense to consider wrapping up the functionality into a CFML Custom Tag.
The code in Listing 12 creates a CF_UseBookRobot
tag that can be used in any template that needs to request information from the Book Robot. It's practically the same as Listing 11, except that most of the variables are in the Attributes
scope (typical for CFML Custom Tags). In addition, a few extra lines of code were added to the CFSCRIPT
block to allow the Custom Tag to add "ISBN" and "Sort" elements to the request structure when appropriate. Finally, the last line of the Custom Tag is what makes the fetched result set visible to the calling template with the name specified in the tag's NAME
parameter.
Tip
This code will make more sense to you if you know the basics about creating CFML Custom Tags. Refer to Chapter ???, "CFML Custom Tags", for a complete discussion on the subject.
Listing 13 - UseBookRobot.cfm - A custom tag that does most of what Listing 11 does
<!--- Tag parameters ---> <CFPARAM NAME="Attributes.Name"> <CFPARAM NAME="Attributes.RobotServer" DEFAULT="#CGI.SERVER_NAME#"> <CFPARAM NAME="Attributes.Columns" DEFAULT=""> <CFPARAM NAME="Attributes.ISBN" DEFAULT=""> <CFPARAM NAME="Attributes.PubDateFrom" DEFAULT=""> <CFPARAM NAME="Attributes.PagesAtLeast" DEFAULT=""> <CFPARAM NAME="Attributes.FirstNames" DEFAULT=""> <CFPARAM NAME="Attributes.LastNames" DEFAULT=""> <CFPARAM NAME="Attributes.Sort" DEFAULT=""> <!--- Assemble the user's input into structure ---> <CFSCRIPT> // New request structure to pass to robot Request = StructNew(); // If tag passed a date, include in structure if (IsDate(Attributes.PubDateFrom)) { Request["PubDateFrom"] = Attributes.PubDateFrom; } // If tag passed a minimum number of pages if (IsNumeric(Attributes.PagesAtLeast)) { Request["PagesAtLeast"] = Attributes.PagesAtLeast; } // If tag passed a specific ISBN number if (Attributes.ISBN is not "") { Request["ISBN"] = Attributes.ISBN; } // If tag passed what columns to fetch if (Attributes.Columns is not "") { Request["Columns"] = Attributes.Columns; } // If tag passed a specific sort order if (Attributes.Sort is not "") { Request["Sort"] = Attributes.Sort; } // Handle any author names passed to tag Authors = ArrayNew(2); for (Counter = 1; Counter LTE ListLen(Attributes.FirstNames); Counter = Counter + 1) { Authors[Counter][1] = ListGetAt(Attributes.FirstNames, Counter); Authors[Counter][2] = ListGetAt(Attributes.LastNames, Counter); } if (ArrayLen(Authors) GT 0) { Request["Author"] = Authors; } </CFSCRIPT> <!--- Convert the structure to WDDX Packet ---> <CFWDDX INPUT="#Request#" OUTPUT="RequestAsWDDX" ACTION="CFML2WDDX"> <!--- Post the WDDX Packet to the "robot" page as a form field ---> <CFHTTP URL="#Attributes.RobotServer#/Robots/BookRobot.cfm" METHOD="POST"> <CFHTTPPARAM NAME="WDDXContent" VALUE="#RequestAsWDDX#" TYPE="FORMFIELD"> </CFHTTP> <!--- Show an error message if CFHTTP tag didn't get anything ---> <CFIF CFHTTP.FileContent is ""> <CFABORT SHOWERROR="Error - No data retrieved from RobotServer"> </CFIF> <!--- Extract recordset from WDDX Packet in robot's response ---> <CFWDDX INPUT="#CFHTTP.FileContent#" OUTPUT="FetchedBooks" ACTION="WDDX2CFML"> <!--- Send fetched recordset to calling template ---> <CFSET "Caller.#Attributes.Name#" = FetchedBooks>
Now the BookFinder.cfm template from Listing 11 can be trimmed down to a very simple template that contains only a few ColdFusion tags. Listing 12 shows the new version of the code, which makes use of the new CF_UseBookRobot
tag to do most of the actual work. The results will look exactly the same as the results from Listing 11 did (see Figure 8), except that each book title now is a link that can be clicked on for more detail.
The only other thing that's been added is that this template now sets a cookie to remember what the user chooses as the "Robot Server" on the search form (see Figure 7). This cookie will be used by the BookFinderDetails.cfm template that gets invoked when the user clicks on a book title (discussed next).
Listing 14 - BookFinder2.cfm - The custom tag keeps everything simple and abstract
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>Book Finder</title> </head> <body> <H2>Book Finder - Results</H2> <!--- Set cookie so "detail" page knows RobotServer ---> <CFSET Cookie.RobotServer = Form.RobotServer> <!--- Custom Tag that interacts w/ Robot via HTTP & WDDX ---> <CF_UseBookRobot ROBOTSERVER="#RobotServer#" PAGESATLEAST="#PagesAtLeast#" PUBDATEFROM="#PubDateFrom#" FIRSTNAMES="#FirstName1#,#FirstName2#,#FirstName3#,#FirstName4#,#FirstName5#" LASTNAMES="#LastName1#,#LastName2#,#LastName3#,#LastName4#,#LastName5#" NAME="FetchedBooks"> <!--- Output the "fetched" books to browser ---> <CFOUTPUT QUERY="FetchedBooks"> <LI><A HREF="BookFinderDetail.cfm?ISBN=#ISBN#">#Title#</A> </CFOUTPUT> </body> </html>
Creating the detail template is now a very simple, since all the work is already encapsulated in the CF_UseBookRobot
Custom Tag. The Custom Tag lets us reuse all of the serialization, CFHTTP, and deserialization code that's already been written. Creating the details pageñshown in Listing 14ñis extremely simple and needs just a few lines of code.
Here, the Custom Tag is used to get the details about a particular book. Because the ROBOTSERVER
parameter is set to #Cookie.RobotServer#
, the user will be communicating with the same Book Robot that the search results came from. Because the COLUMNS
attribute is set to "*"
, the Rook Robot will do a SELECT *
type of query from the database, so all the information from the table (the Title column, the Description column, and so on) will be available for display in the CFOUTPUT
block. The results are shown in Figure 9.
Listing 15 - BookFinderDetail.cfm - Reusing the Book Robot and Custom Tag for a "details" page
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>Book Finder</title> </head> <H2>Book Finder - Detail</H2> <body> <!--- Use our Custom Tag to get all data for this ISBN ---> <CF_UseBookRobot ROBOTSERVER="#Cookie.RobotServer#" ISBN="#URL.ISBN#" NAME="BookDetail" COLUMNS="*"> <!--- Output the information to the browser ---> <CFOUTPUT QUERY="BookDetail"> <!--- Make TH table headers gray (in 4.0+ browsers) ---> <STYLE> TH {background=silver}; </STYLE> <TABLE BORDER="1"> <TR> <TH>ISBN</TH><TD>#ISBN#</TD> <TH>Author</TH><TD>#AuthorLastName#, #AuthorFirstName#</TD> </TR> <TR> <TH COLSPAN="2">Title</TH><TD COLSPAN="2">#Title#</TD> </TR> <TR> <TH COLSPAN="2">Description</TH><TD COLSPAN="2">#Description#</TD> </TR> </TABLE> </CFOUTPUT> </body> </html>
Figure 9 - This details page appears when the user clicks a title in the search results
This section created a "Book Robot" and showed how its use can be "wrapped up" into a Custom Tag, to the point where accessing data via the Book Robot becomes no more complicated than accessing a conventional data source (with a normal CFQUERY
tag in the actual "searching" or "details" templates).
But now that it's done, just think of what it means. When a new A2Z store opens anywhere in the world, all they need to do to allow their inventory to be searched by the other stores is to make the BookRobot.cfm template available on their webserver. The other stores need only add that store's server to the "Location" drop-down list on the search form. Instant, inexpensive access!
The Book Robotñand the pages that fetch data from itñis really a "distributed" or "three-tier" application. This kind of thinkingñthat web pages can act as "data slaves" to other web pages, applications, or development toolsñhas the potential to become more and more prevalent in the near future. WDDX makes the development of such systems much, much easier.
And it keeps getting better, because not only ColdFusion applications can interact with the BookFinder. Any application that can fetch a web page and interpret WDDX packets can use its services as well. You'll see it in action with Javascript later on in this chapter.
Note
The Book Robot and the templates that use it were designed to demonstrate using WDDX with structures and arrays, and to demonstrate using WDDX in two directions (both to and from the robot). In practice, the process could be simplified somewhat. First, the fact that the author names to search for are expected to be passed as an array (rather than as a simple list, or only allowing the user to search for one author at a time) makes it necessary to go through some extra steps to create the 2-dimensional array from the various blanks on the form. Also, it might be a bit simpler to have the robot expect the search parameters as individual form variables instead of wrapping up all of the parameters into a WDDX Packet. Each parameter would be passed as a separate CFHTTPPARAM
tag in the "finder" template, and only the resulting recordset would need to be converted to the WDDX format and back again.
As you heard a few times earlier in this chapterñbut have yet to actually seeñWDDX is not only about getting data between ColdFusion pages. It is also about passing data between any application for which there WDDX support exists. ColdFusion is obviously one such application, but Allaire also provides support for WDDX for Javascript.
This makes it very easy to allow Javascript to be aware of things like query result sets, structures, and arrays that traditionally have only been accessible "on the server side". For instance, when you run a query with the CFQUERY
tag, you of course have always been able to send the information to the browser as a dynamically-produced web page. That's what ColdFusion has always been all about. WDDX lets you go a step further by letting making the recordset itself a scriptable object. This really lets your web interfaces "come alive" in a way that would have required a lot of very clever coding in the past.
Note
This section assumes that you have at least a bit of familiarity with Javascript. You certainly don't need to know the language inside and out to follow along, but if you have never dealt with Javascript at all before, you might want to at least skip some sort of "primer" document about the basics. Documentation about Javascript is available online at http://developer.netscape.com
Tip
Don't get confused between Javascriptñwhich is what this section deals withñand Java. The names are similar because they were originally conceived to be used together, but they are very different things. Javascript is a simple interface-scripting language with support for WDDX today. Java is a much more complicated monster, and does not have WDDX support at the time of this writing.
Using the CFWDDX
tag, ColdFusion can automatically write Javascript code for you which translates whatever data you have in a WDDX packet into native Javascript variables. For instance, if you have set a ColdFusion variable to hold a simple string, you already know how to convert that variable into a WDDX Packet. You can then run the packet through the CFWDDX
tag again, this time to convert the WDDX Packet into Javascript code. That Javascript code, when interpreted by the browser, will set a Javascript variable to the string value that was contained within the WDDX Packet.
#Message#
variable. You've already seen the top part of the templateñit converts the contents of the #Message#
variable to a WDDX Packet named MyWDDXPacket
.
The new part of the code uses the CFWDDX
tag to convert the packet from the WDDX format to Javascript code, by supplying the MyWDDXPacket
variable to the tag's INPUT
parameter. The TOPLEVELVARIABLE
parameter tells the CFWDDX tag to write the Javascript code such that it creates a Javascript variable named MyJSVariable
. Later, you'll be able to use your browser's "View Source" command to see the Javascript code generated by the CFWDDX tag.
The Javascript code is placed in the variable called DynamicJSCode
, since that's what is specified in the tag's OUTPUT
parameter. The variable is then output between a pair of SCRIPT
tags, which tell the browser to interpret everything between them as Javascript code. When the browser interprets the Javascript code, the Javascript variable called MyJSVariable
will be set to the original WDDX packet's contents.
Finally, this template uses Javascript's alert
method to display the Javascript variable's contents in a pop-up dialog box, proving that the variable was passed successfully. The resulting web page is exactly the same as before, except for the pop-up message. Figure 10 shows the results.
Listing 16 - JSSerialize.cfm - Making Javascript aware of a ColdFusion variable
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>Message Serializer</title> </head> <body> <H2>Message Serializer</H2> <!--- Set the #Message# variable to a simple string value ---> <CFSET Message = "Hello, World!"> <!--- Serialize the #Message# variable into a WDDX Packet ---> <CFWDDX INPUT="#Message#" OUTPUT="MyWDDXPacket" ACTION="CFML2WDDX"> <!--- Output WDDX Packet so we can see what it looks like ---> <!--- (HTMLEditFormat function lets us see tags properly) ---> <CFOUTPUT> <P><B>Original Message:</B> #Message#</P> <P><B>The message was serialized into the following WDDX Packet:</B></P> #HTMLEditFormat(MyWDDXPacket)# </CFOUTPUT> <!--- **** new Code starts here **** ---> <!--- Ask CFWDDX tag to convert packet from WDDX to Javascript ---> <!--- Resulting Javascript code gets placed in #DynamicJSCode# ---> <CFWDDX INPUT="#MyWDDXPacket#" OUTPUT="DynamicJSCode" ACTION="WDDX2JS" TOPLEVELVARIABLE="MyJSVariable"> <!--- Output dynamically-produced Javascript code from CFWDDX ---> <!--- Must be between SCRIPT tags so JS knows to interpret it ---> <SCRIPT LANGUAGE="JAVASCRIPT"> // The following code was produced by the CFWDDX tag. // It will set a variable named "MyJSVariable" to the // Contents of the WDDX Packet supplied to 2nd CFWDDX <CFOUTPUT>#DynamicJSCode#</CFOUTPUT> // Now display the variable in a pop-up dialog box alert(MyJSVariable); </SCRIPT> </body> </html>
Tip
Always remember to use CFOUTPUT
tags around a variable that contains dynamic Javascript codeñsuch as the DyanamicJSCode
variable in this exampleñbetween SCRIPT
tags. Otherwise, the actual name of the variable (with the #
signs around it and all) will be sent to the browser. Since the variable name is not valid Javascript code (only its contents are), this will cause the browser to "choke" and display a Javascript error message.
Figure 10 - The web page itself is now aware of the "message" variable
If you use your browser's "View Source" command so that you can see the final page code that was sent to the browser, you can see the Javascript that the CFWDDX
added to the page. Listing 16 shows the SCRIPT
portion of the page's source code. Notice that the generated Javascript code is very simple, and is exactly what you would probably have written if you wanted to assign the string "Hello, World!" to a variable named MyJSVariable.
Listing 17 - JSSerialize-SourceSnippet.html - The Javascript code that Listing 16 produces
<SCRIPT LANGUAGE="JAVASCRIPT"> // The following code was produced by the CFWDDX tag. // It will set a variable named "MyJSVariable" to the // Contents of the WDDX Packet supplied to 2nd CFWDDX MyJSVariable="Hello, World!"; // Now display the variable in a pop-up dialog box alert(MyJSVariable); </SCRIPT>
CFWDDX
tagsñone to convert from CFML to WDDX, and a second to convert from WDDX to Javascript. The CFWDDX
tag allows you to use ACTION="CFML2JS"
, which converts a ColdFusion value directly to Javascript without needing to code the WDDX conversion as a separate step. This is a "shortcut" to save you a step while coding. Internally, the CFWDDX
tag is behaving just as if you used it twice, as we did in Listing 15.
Listing 17 shows how the code from Listing 15 can be reduced to just a few tags. Notice that the two CFWDDX
tags have been consolidated into one. Also, the OUTPUT
parameter has been eliminated from the CFWDDX
tag, which causes it to spit out the Javascript code right then, between the SCRIPT
tags. This saves you the step of having to output the DynamicJSCode
variable yourself. This template will behave the same way that the previous one did (Figure 10), except that it doesn't bother outputting the message and the original WDDX packet to the user.
Listing 18 - JSSerialize2.cfm - Simplifying matters by going straight from CFML to Javascript
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>Message Serializer</title> </head> <body> <H2>Message Serializer</H2> <!--- Set the #Message# variable to a simple string value ---> <CFSET Message = "Hello, World!"> <!--- Ask CFWDDX tag to convert packet from CFML to Javascript ---> <SCRIPT LANGUAGE="JAVASCRIPT"> // The following code was produced by the CFWDDX tag. // It will set a variable named "MyJSVariable" to the // Contents of the ColdFusion #Message# variable <CFWDDX INPUT="#Message#" ACTION="CFML2JS" TOPLEVELVARIABLE="MyJSVariable"> // Now display the variable in a pop-up dialog box alert(MyJSVariable); </SCRIPT> </body> </html>
Tip
When you leave out the CFWDDX
tag's OUTPUT
parameter like this (so the tag inserts its Javascript code at the current spot in the template), always make sure to place the CFWDDX
tag between SCRIPT
tags. Otherwise, the browser won't know to interpret the code as Javascriptñinstead, it will just display the code on the page as if it were ordinary text.
You've seen how the CFWDDX tag can be used to "transport" a variable from ColdFusion to Javascript. It's certainly interesting, but not exactly exciting. For instance, you probably have already realized that you could have used a simple CFOUTPUT tag to create that same Javascript code that the CFWDDX tag created. So what's the big deal?
Well, again, as you saw earlier in this chapter, WDDX can pass simple variablesñlike numbers, strings, and datesñaround just fine, it only gets really interesting when you start using it to pass complex datatypes around, like arrays, structures, and especially recordsets.
Take a look at Listing 18. It creates a web page with a simple form on it. It runs a query to get the ISBN number for each book in the A2Z database's Inventory table. It then uses the CFWDDX tag to convert the query's results to a Javascript wddxRecordset object named Books. A wddxRecordset object contains a separate array for each query column, indexed by row number. Which means that if you needed to get the Title from the fifth row of the recordset, you could refer to Books.title[5]
in your Javascript code. Similarly, Books.isbn[1]
would return the ISBN number of the first row returned by the query.
The scripting part of the template then goes on to create a user-defined function called InitControls()
, which is responsible for populating the SELECT
box in the simple form at the bottom of the template. This is a pretty simple function, but if you're not familiar with Javascript it may be confusing. First of all, BookID.options.length
is set to zero. The options property of the BookID
object represents the available choices in the SELECT
box. Setting it to zero simply clears any choices that may currently be sitting in the SELECT
box.
Next, the SELECT
box is populated by the code inside the for
loop. Basically, the for statement says to execute the loop once for each row in the query recordset, incrementing the RowNum
variable by one each time through the loop. When the RowNum
variable reaches the length of the Books.bookid
array (which is the number of rows in the query), the loop will stop. Inside the loop, a new Option object called NewOpt
is created, which is a special type of object that represents a choice in a SELECT
box. The "value" of the new option (the value that will be submitted by the form) is given the BookID
from the current row of the recordset, and the "text" of the new option (the value that the user sees) is given the Title of the current row of the recordset. Finally, the new option is added to the array of options in the BookID
object (the SELECT
box itself).
When you first bring this page up in a browser, the SELECT
box will not have any book choices in it. When you click the "Populate Drop-Down" button, the InitControls()
function is executed, which fills the SELECT
box with the information about the books fetched by the original CFQUERY
. Figure 11 shows what the form looks like after the button is pressed.
Tip
Always consider using the with
statement to make your Javascript code simpler and easier to read. For instance, this template uses the with(document.dataForm)
statement to say that every object referred to within the curly braces should be assumed to be an element of the dataForm object. Without the with
statement, each reference to the BookID
object would need to be typed in its "fully-qualified" form: document.dataform.BookID
.
Tip
Always remember that Javascript is case-sensitive. So since the function is defined (near the top of the SCRIPT
block) as InitControls()
, that spelling must be used exactly to invoke the function when you refer to it elsewhere. For instance, if you used different capitalization in the onLoad
parameter of the document's BODY
tag, you would get a Javascript error, complaining that the function does not exist.
Note
When you refer to a column of a recordset in Javascript code, you must always type the column's name in lowercase. Since ColdFusion is not case-sensitive (and thus does not necessarily know the exact capitalization of your database's column names), but Javascript is case-sensitive, the folks at Allaire needed to make a decision about whether column names would get implemented in all-uppercase or all-lowercase. They chose all-lowercase, presumably because other Javascript stuff (like the names of all the functions and methods) is generally in lowercase as well.
Listing 19 - JSBrowser1.cfm - Making an entire query recordset available to Javascript
<!--- Get data about books from database ---> <CFQUERY NAME="GetBooks" DATASOURCE="A2Z"> SELECT BookID, Title, ISBN FROM Inventory ORDER BY Title </CFQUERY> <HTML><HEAD> <TITLE>Book Browser</TITLE> <!--- Include Allaire's WDDX / Javascript support ---> <SCRIPT src="/cfide/scripts/wddx.js" LANGUAGE="JAVASCRIPT"></SCRIPT> <SCRIPT LANGUAGE="JAVASCRIPT"> <!--- Convert query to Javascript object named "Books" ---> <CFWDDX ACTION="CFML2JS" INPUT="#GetBooks#" TOPLEVELVARIABLE="Books"> function InitControls() { // Everything in this "with" block pertains to the form with (document.DataForm) { // Clear any current OPTIONS from the SELECT BookID.options.length = 0; // For each book record... for (var RowNum = 1; RowNum < Books.bookid.length; RowNum++) { // Create a new OPTION object NewOpt = new Option; NewOpt.value = Books.bookid[RowNum]; NewOpt.text = Books.title[RowNum]; // Add the new object to the SELECT list BookID.options[BookID.options.length] = NewOpt; } } } </SCRIPT> </HEAD> <BODY BGCOLOR="SILVER"> <H2>Offline Book Browser</H2> <!--- Start building a Javascript-enabled form ---> <FORM NAME="DataForm"> <!--- Gets populated by InitControls() function ---> <SELECT NAME="BookID" SIZE="10"> <OPTION>======================================== </SELECT> <!--- Button to execute the InitControls() function ---> <P><INPUT TYPE="BUTTON" VALUE="Populate Drop-Down" onClick="InitControls()"> </FORM> </BODY> </HTML>
Tip
Always include LANGUAGE="JAVASCRIPT"
in your SCRIPT
tags, even though it's technically optional as far as HTML is concerned. Since Microsoft Internet Explorer understands both Javascript and VBScript, you don't want it to try and guess which of the two languages your script code is written in. So it's good practice to always specify the LANGUAGE
in any SCRIPT
tag.
Tip
If you ever get a message from Javascript that says something like "wddxRecordset is undefined", you probably forgot to include a reference to the wddx.js script fileñlike SCRIPT src
tag near the top of Listing 18. Make sure that the tag comes before any ColdFusion-generated Javascript code that talks about query recordsetsñwhich probably means putting it before a CFWDDX
tag.
Figure 11 - The SELECT box in this form is populated by Javascript when the page loads
Since Javascript is now aware of the results returned by the GetBooks query, it becomes very easy to add some client-side scripting to make the form "dynamic". Since Javascript knows which Author, Title, and so on goes with each BookID, you can let your users browse through datañdisplaying related information as they do soñwith a minimum of coding.
Take a look at Listing 19, shown in Figure 12. It takes the code from Listing 18 and adds text input boxes for the ISBN, Title, and Author name for each book. Two user-defined Javascript functions have also been added to make the form "come alive". The functions are astonishingly simple, mainly because Allaire's wddxRecordset object makes it so simple to access a recordset's values.
The FillControls()
function fills the four text boxes with the Title, Author, and so on for the currently-selected book in the SELECT
list. Since the FillControls()
function is referred to in the SELECT
tag's onChange
handler, the function will execute whenever the user chooses a different book from the list. The function itself is extremely simpleñit just sets a variable called RowNum that represents the currently-selected book. Then the function assigns the appropriate element of the Books
recordset object to the Value
property of the appropriate text box. For instance, to fill the text box named Title, the script simply sets Title.value
to whatever data is in Books.title[RowNum]
.
The KeepChanges()
function does just the opposite. It reads the values from the four text boxes and places their values into the appropriate spots in the Books
wddxRecordset object. Next, it executes the InitControls()
function (to "re-draw" the items in the SELECT
box, causing the new record to appear as the last choice in the SELECT
box). Then it sets the SELECT
box's selectedIndex
property to -1 (which causes none of the choices to be selected for a split second), then immediately sets selectedIndex
back to the choice that was selected before the function was called. The function is assigned to the "Keep These Edits" button by referring to the function's name in the INPUT
tag's onClick
handler.
Listing 20 - JSBrowser2.cfm - Populating related controls at run-time from a recordset
<!--- Get data about books from database ---> <CFQUERY NAME="GetBooks" DATASOURCE="A2Z"> SELECT BookID, Title, ISBN, AuthorFirstName, AuthorLastName FROM Inventory ORDER BY Title </CFQUERY> <HTML><HEAD> <TITLE>Book Browser</TITLE> <!--- Include Allaire's WDDX / Javascript support ---> <SCRIPT src="/cfide/scripts/wddx.js" LANGUAGE="JAVASCRIPT"></SCRIPT> <SCRIPT LANGUAGE="JAVASCRIPT"> <!--- Convert query to Javascript object named "Books" ---> <CFWDDX ACTION="CFML2JS" INPUT="#GetBooks#" TOPLEVELVARIABLE="Books"> //////////////////////////////////////////////////// // This function fills the SELECT list with books function InitControls() { with (document.DataForm) { // Clear any current OPTIONS from the SELECT BookID.options.length = 0; // For each book record... for (var i = 1; i < Books.getRowCount(); i++) { // Create a new OPTION object NewOpt = new Option; NewOpt.value = Books.bookid[i]; NewOpt.text = Books.title[i]; // Add the new object to the SELECT list BookID.options[BookID.options.length] = NewOpt; } } } //////////////////////////////////////////////// // This function populates other INPUT elements // when an option in the SELECT box is clicked function FillControls() { with (document.DataForm) { // Add one to the OPTION number to get the data row number var RowNum = BookID.selectedIndex+1; // Populate textboxes with data in that row ISBN.value = Books.isbn[RowNum]; Title.value = Books.title[RowNum]; AuthorF.value = Books.authorfirstname[RowNum]; AuthorL.value = Books.authorlastname[RowNum]; } } //////////////////////////////////////////////// // This function "saves" data from the various // text boxes into the wddxRecordset object function KeepChanges() { with (document.DataForm) { // Add one to the OPTION number to get the data row number var SelectedBook = BookID.selectedIndex; var RowNum = SelectedBook + 1; // Populate javascript data array with info from textboxes Books.isbn[RowNum] = ISBN.value; Books.title[RowNum] = Title.value; Books.authorfirstname[RowNum] = AuthorF.value; Books.authorlastname[RowNum] = AuthorL.value; // Re-initialize the SELECT list InitControls(); // Re-select the book that was selected before BookID.selectedIndex = SelectedBook; } } </SCRIPT> </HEAD> <!--- After document loads, run InitControls() function ---> <BODY onLoad="InitControls();"> <H2>Offline Book Browser</H2> <FORM ACTION="Client2.cfm" METHOD="POST" NAME="DataForm"> <TABLE BORDER CELLPADDING="10"> <TR VALIGN="TOP"> <TD> <!--- SELECT populated by InitControls() function ---> <!--- When clicked, calls FillControls() function ---> <SELECT NAME="BookID" SIZE="10" onChange="FillControls()"> <OPTION>============= (loading) ================= </SELECT> </TD> <TD> <!--- These controls get populated by FillControls() ---> <B>ISBN:</B><BR> <INPUT NAME="ISBN" SIZE="20" MAXLENGTH="13"><BR> <B>Author (first, last):</B><BR> <INPUT NAME="AuthorF" SIZE="18" MAXLENGTH="13"> <INPUT NAME="AuthorL" SIZE="20" MAXLENGTH="13"><BR> <B>Title:</B><BR> <INPUT NAME="Title" SIZE="40" MAXLENGTH="50"><BR> <P> <!--- Button to "keep" edits with KeepChanges() function ---> <INPUT TYPE="BUTTON" VALUE="Keep These Edits" onClick="KeepChanges()"> <!--- Button to cancel edits with FillControls() function ---> <INPUT TYPE="BUTTON" VALUE="Cancel" onClick="FillControls()"><BR> </TD> </TR> </TABLE> </FORM> </BODY> </HTML>
Note
In the FillControls()
function, it was necessary to add one to the selectedIndex
property of the BookID
object in order for the RowNum
variable to represent the correct row in the recordset. This is because the options in a select box are numbered beginning with zero, but the rows in a recordset are numbered beginning with one. So if the uses clicks the fourth item in the select box, the selectedIndex
property will be 3 but the appropriate row in the recordset is 4.
What all this means is that the user can browse through the records and make changes to the data, all without any interaction at all with the server. This is the kind of effect that would generally have taken a lot more work to do in the past. In fact, many ColdFusion developers don't even try to put together this kind of form, because the kind of Javascript code that the CFWDDX
tag generated can take a while to come up with from scratch. Instead, they generally put together some kind of solutionñoften using "invisible" framesñthat makes a round-trip to the server to get the detail information for each data record that the user wants to look at.
So, by leveraging CFWDDX
and Javascript, you may be able to cut down or eliminate frequent trips to the server. This reduces the load on your server, makes the application run faster, and provides zippier performance for your users.
Note
At this point, when users use the "Keep These Edits" button, they are only changing the data in the Books wddxRecordset. That is, they are only changing the "local copy" of the recordset that's in the browser's little brain. The actual database on the server has not been updated. The next code listing will do that.
Figure 12 - When the user clicks an item in the list, the other inputs are filled with the corresponding information
Of course, the Book Browser application would not be complete without a way to actually commit the user's changes to the database on the server. Additionally, it would be nice if the user could data-enter new books using the same screen. These new records would also need to be inserted into the database on the server.
Allaire provides a number of Javascript functions for working with WDDX from Javascript code. All of the functions are defined in the wddx.js Javascript source file. Each of the Book Browser examples in this chapter include these functions with the SCRIPT
tag that appears near the top of the templates.
The wddx.js file defines two custom Javascript objects. The first object that it defines is the wddxSerializer object, which is used to serialize data into WDDX Packets. This object has only one function available for your use, the serialize
function. This function takes any Javascript variable or object as its argument and returns the appropriate WDDX Packet as a Javascript string value.
The second object defined in wddx.js is the wddxRecordset object, which represents a data recordset structure. A recordset is the Javascript equivalent of a ColdFusion query result set. In other words, when you use CFWDDX
to convert a CFQUERY
tag's results to Javascript, you are creating a wddxRecordset object that contains the same columns and rows returned by the original query. Allaire provides a number of functions that you can use to manipulate the data in wddxRecordset objects.
Tip
You can find more information about the recordset-manipulation functions in the WDDX JavaScript Objects section of the CFML Language Reference, which is part of the documentation that Allaire ships with ColdFusion Application Server.
Table 1 shows example syntax to use when creating new wddxSerializer and wddxRecordset objects, and shows how to use the various Allaire-defined functions. The code snippets are taken almost exactly from the code listings that follow during the rest of this chapter.
Table 1 - Javascript functions for WDDX serialization and recordset manipulation
Javascript syntax | What it does |
MySerializer = new wddxSerializer |
Creates a new wddxSerializer object named MySerializer . Required before you can use the serialize function (below). |
MyPacket = MySerializer.serialize(MyJSVariable) |
Serializes the Javascript variable or object named MyJSVariable and places the resulting WDDX Packet into the string variable named MyPacket . |
Books = new wddxRecordset |
Creates a new, empty wddxRecordset object named Books . In general, you will not need to create a recordset object from scratch like this, because the CFWDDX tag creates the recordset for you if you use it to convert a ColdFusion query to Javascript. |
Books.setField(3, "title", "Moby Dick") - or - Books.title[3] = "Moby Dick" |
Sets the third row of the title column to "Moby Dick". You could use any simple value in place of "Moby Dick", like a Javascript number or date variable. |
MyTitle = Books.getField(3, "title") - or - MyTitle = Books.title[3] |
Sets the MyTitle variable to whatever value is currently in the third row of the Books recordset's title column. |
Books.addRows(1) | Adds one row to the recordset object named Books. The new row is added to the end of the recordset. All of the columns in the row will be blank unless you set values to them. |
Books.addColumn("wasedited") |
Adds a column named wasedited to the Books recordset. All of the rows in the column will be blank unless you set values to them. |
NumRows = Books.getRowCount() | Sets the NumRows variable to the number of rows currently in the Books recordset. |
Note
The wddx.js file is placed into the CFIDE/Scripts subfolder in your webserver's document root for you when you install ColdFusion. The examples in this chapter assume that the file has not been moved and can be accessed by a browser at the absolute URL \CFIDE\Scripts\wddx.js
on your webserver. If you have moved or deleted the CFIDE folder, or if you are in some kind of "virtual domain" or "hosted server" situation such that the file is not available at that URL, you can just place a copy of the wddx.js file in the same folder as your actual ColdFusion templates (.cfm files). Then remove the path information from the SCRIPT
tag's src
parameter so that it just reads "src=wddx.js"
, indicating to the browser that the file can be found in the current directory.
Tip
If you're curious about how the various functions are defined, open the wddx.js file up in a text editor. You'll notice that a number of other functions are defined for the wddxSerializer object that are not being discussed in this chapter. For instance, there is a wddxSerializer_serializeValue
function and a wddxSerializer_serializeString
function. Just ignore all of the serializing functions except for the serialize
function, which is the only function that's intended for our use as ColdFusion developers. The other functions are used internally by the serialize
function.
Take a look at Listing 20, the results of which are shown in Figure 13. It adds two additional Javascript functions in the SCRIPT
portion of the template, with a button to invoke each function. Specifically, a "New Record" button has been added to the form, which executes the NewRecord()
function when pressed. Also, a "Commit Changes To Server" button has been added under the editing portion of the page, which executes the CommitToServer()
function when pressed. Let's take a closer look at these two functions.
The template's NewRecord()
function uses three of the functions that Allaire supplies for use with wddxRecordset objects (like the Books
object in this example). First, it uses the addRows
function to add one new row to the Books
recordset. The new row is added to the bottom of the recordset. Next, it uses the getRowCount
function to set a variable named NewRow
, which will hold the row number of the just-added row. Then it uses the setField
function to set each column of the new row to some initial values. Note that the BookID
column is set to the string "new". This will indicate to the next template that the record is a new record and thus should be inserted (rather than updated) to the database. Finally, the function redraws the SELECT
list with the InitControls()
function, sets its selectedIndex
so that the new record appears "selected" in the form, and calls the FillControls()
function so that the data-entry inputs get filled with the new (mostly blank) values.
The template's CommitToServer()
function is in charge of serializing the recordset into a new WDDX Packet, then placing the packet in a hidden field and submitting the form. Thanks to the wddx.js file that Allaire provides, the serializing part requires only two lines of Javascript code. First, a new "WDDX Serializer" object called MySerializer
is created, with the help of Javascript's new
keyword. This step is necessary whenever you want to serialize a value from Javascript. Next, the serialize function of the MySerializer object is used to serialize the Books
recordset into a WDDX Packet, placing the packet into a Javascript variable called BooksAsWDDX
. Then the function sets uses the new variable to place the packet's contents into a hidden form field called WDDXContent. Finally, the function submits the form.
The end result is that the ColdFusion template that this form submits to (JSBrowserCommit.cfm) will be able to refer to a variable called #Form.WDDXContent#
. The variable will hold the WDDX Packet that contains the edited version of the recordset.
Listing 21 - JSBrowser3.cfm - Allowing the user to insert new records and commit to database
<!--- Get data about books from database ---> <CFQUERY NAME="GetBooks" DATASOURCE="A2Z"> SELECT BookID, Title, ISBN, AuthorFirstName, AuthorLastName FROM Inventory ORDER BY Title </CFQUERY> <HTML><HEAD> <TITLE>Book Browser</TITLE> <!--- Include Allaire's WDDX / Javascript support ---> <SCRIPT src="/cfide/scripts/wddx.js" LANGUAGE="JAVASCRIPT"></SCRIPT> <SCRIPT LANGUAGE="JAVASCRIPT"> <!--- Convert query to Javascript object named "Books" ---> <CFWDDX ACTION="CFML2JS" INPUT="#GetBooks#" TOPLEVELVARIABLE="Books"> // Add a column called "wasedited" to the recordset // A "Yes" in this column means the row was "touched" Books.addColumn("wasedited"); //////////////////////////////////////////////////// // This function fills the SELECT list with books function InitControls() { with (document.DataForm) { // Clear any current OPTIONS from the SELECT BookID.options.length = 0; // For each book record... for (var i = 1; i < Books.getRowCount(); i++) { // Create a new OPTION object NewOpt = new Option; NewOpt.value = Books.bookid[i]; NewOpt.text = Books.title[i]; // Add the new object to the SELECT list BookID.options[BookID.options.length] = NewOpt; } } } //////////////////////////////////////////////// // This function populates other INPUT elements // when an option in the SELECT box is clicked function FillControls() { with (document.DataForm) { // Add one to the OPTION number to get the data row number var RowNum = BookID.selectedIndex+1; // Populate textboxes with data in that row ISBN.value = Books.isbn[RowNum]; Title.value = Books.title[RowNum]; AuthorF.value = Books.authorfirstname[RowNum]; AuthorL.value = Books.authorlastname[RowNum]; } } //////////////////////////////////////////////// // This function "saves" data from the various // text boxes into the wddxRecordset object function KeepChanges() { with (document.DataForm) { // Add one to the OPTION number to get the data row number var SelectedBook = BookID.selectedIndex; var RowNum = SelectedBook + 1; // Populate javascript data array with info from textboxes Books.isbn[RowNum] = ISBN.value; Books.title[RowNum] = Title.value; Books.authorfirstname[RowNum] = AuthorF.value; Books.authorlastname[RowNum] = AuthorL.value; Books.wasedited[RowNum] = 'Yes'; // Re-initialize the SELECT list InitControls(); // Re-select the book that was selected before BookID.selectedIndex = -1; BookID.selectedIndex = SelectedBook; } } //////////////////////////////////////////////// // This function inserts a new row in the // wddxRecordset object, ready for editing function NewRecord() { with (document.DataForm) { // Add a new row to the recordset Books.addRows(1); NewRow = Books.getRowCount()-1; Books.setField(NewRow, "bookid", "new"); Books.setField(NewRow, "title", "(new)"); Books.setField(NewRow, "isbn", ""); Books.setField(NewRow, "authorfirstname", ""); Books.setField(NewRow, "authorlastname", ""); // Re-initialize the SELECT list InitControls(); // Re-select the book that was selected before BookID.selectedIndex = NewRow-1; FillControls(); } } //////////////////////////////////////////////// // This function inserts a new row in the // wddxRecordset object, ready for editing function CommitToServer() { with (document.DataForm) { // Create new WDDX Serializer object (supplied by Allaire) MySerializer = new WddxSerializer(); // Serialize the "Books" recordset into a WDDX packet BooksAsWDDX = MySerializer.serialize(Books); // Place the packet into the "WDDXContent" hidden field WDDXContent.value = BooksAsWDDX; // Submit the form submit(); } } </SCRIPT> </HEAD> <!--- After document loads, run InitControls() function ---> <BODY onLoad="InitControls();"> <H2>Offline Book Browser</H2> <FORM ACTION="JSBrowserCommit.cfm" METHOD="POST" NAME="DataForm"> <!--- CommitToServer() function gives this a value ---> <INPUT TYPE="HIDDEN" NAME="WDDXContent"> <TABLE BORDER CELLPADDING="10"> <TR VALIGN="TOP"> <TD> <!--- SELECT populated by InitControls() function ---> <!--- When clicked, calls FillControls() function ---> <SELECT NAME="BookID" SIZE="12" onChange="FillControls()"> <OPTION>============= (loading) ================= </SELECT> </TD> <TD> <!--- These controls get populated by FillControls() ---> <B>ISBN:</B><BR> <INPUT NAME="ISBN" SIZE="20" MAXLENGTH="13"><BR> <B>Author (first, last):</B><BR> <INPUT NAME="AuthorF" SIZE="18" MAXLENGTH="13"> <INPUT NAME="AuthorL" SIZE="20" MAXLENGTH="13"><BR> <B>Title:</B><BR> <INPUT NAME="Title" SIZE="40" MAXLENGTH="50"><BR> <P> <!--- Button to "keep" edits with KeepChanges() function ---> <INPUT TYPE="BUTTON" VALUE="Keep These Edits" onClick="KeepChanges()"> <!--- Button to cancel edits with FillControls() function ---> <INPUT TYPE="BUTTON" VALUE="Cancel" onClick="FillControls()"> <!--- Button to insert new book with NewRecord() function ---> <INPUT TYPE="BUTTON" VALUE="New Record" onClick="NewRecord()"><BR> </TD> </TR> </TABLE> <!--- Button to save to server w/ CommitChanges() function ---> <P><CENTER> <INPUT TYPE="BUTTON" VALUE="Commit Changes To Server" onClick="CommitToServer()"><BR> </CENTER> </FORM> </BODY> </HTML>
Figure 13 - This version of the page allows the user to insert records and update the actual datbase
The JSBrowserCommit.cfm template that receives the WDDX Packet from the Book Browser is actually quite simple. Since the packet's contents were stored in the hidden field named WDDXContent just before the form was submitted, the packet will be available to this template in the #Form.WDDXContent#
variable. All the template needs to do is use CFWDDX to deserialize the packet into a query recordset named EditedBooks
. Then it can use a CFLOOP over the query to quickly examine each data row to see if it is a new or changed record.
If the BookID
column of the current record is set to the string "new", then the template knows that the record was inserted by the Book Browser's NewRecord()
function. Therefore, it runs a simple INSERT
query to insert the new row into the Inventory table.
If the BookID
column of the current record is not set to "new", the template checks to see if the WasEdited
column has been set to "Yes". If it has, then the template knows that the record was edited by the Book Browser's KeepChanges()
function. Therefore, it runs a simple UPDATE
query to update the corresponding row in the Inventory table, using the BookID
column as the primary key.
Finally, the template displays a simple message to let the user know that the records were inserted or updated successfully. A summary is provided that shows the number of inserted records and the number of updated records. Figure 14 shows the results.
Listing 22 - JSBrowserCommit.cfm - Receiving the edited recordset and updating the database
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>Untitled</title> </head> <body> <H2>Committing Changes to Database</H2> <!--- Convert the incoming WDDX Packet to "EditedBooks" query ---> <CFWDDX INPUT="#Form.WDDXContent#" OUTPUT="EditedBooks" ACTION="WDDX2CFML"> <!--- We'll increment these counters in the loop ---> <CFSET InsertCount = 0> <CFSET UpdateCount = 0> <!--- Loop over each of the records in the query ---> <CFLOOP QUERY="EditedBooks"> <!--- If it's a new book (the user inserted it) ---> <CFIF EditedBooks.BookID is "new"> <CFQUERY DATASOURCE="A2Z"> INSERT INTO Inventory (ISBN, Title, AuthorFirstName, AuthorLastName) VALUES ('#ISBN#', '#Title#', '#AuthorFirstName#', '#AuthorLastName#') </CFQUERY> <CFSET InsertCount = InsertCount + 1> <!--- It's an existing book (user may have edited) ---> <CFELSEIF EditedBooks.WasEdited is "Yes"> <CFQUERY DATASOURCE="A2Z"> UPDATE Inventory SET ISBN = '#ISBN#', Title = '#Title#', AuthorFirstName = '#AuthorFirstName#', AuthorLastName = '#AuthorLastName#' WHERE BookID = #BookID# </CFQUERY> <CFSET UpdateCount = UpdateCount + 1> </CFIF> </CFLOOP> <!--- Display message about what exactly happened ---> <CFOUTPUT> <P><B>Changes Committed!</B> <UL> <LI>Records Updated: #UpdateCount# <LI>Records Inserted: #InsertCount# </UL> </CFOUTPUT> </body> </html>
Figure 14 - New records get inserted into the database, and changed records get updated
Currently, Allaire's Javascript support for WDDX does not provide a way to deserialize a WDDX Packet. That is, when you use the CFWDDX
tag to convert a ColdFusion variable to Javascript, you are not passing a WDDX Packet to Javascript, which Javascript then deserializes into native Javascript objects. Instead, Javascript code is written for you "on the fly" that creates the native Javascript objects from scratch. The effect is the same, and puts much less stress on the browser. It's a good thing.
Still, you may come across a few situations where your Javascript application has a WDDX Packet that it needs to deserialize. Using the hidden-form-field technique from Listing 21, you could have the application post the packet to an "intermediate" ColdFusion template. The template could then use the CFWDDX tag to send the deserialized values back to Javascript.
Simple enough, but if something about your application makes that intermediate step very undesirable, there is a WDDXDeserialize
function available that you can execute from Javascript code. The function is not distributed with ColdFusion, and was not created by Allaire. It was written by Nate Weiss, the author of this chapter. You'll find the WDDXDeserialize
function in the wddxDes.js file which is included on the book's CD for your convenience. The next code listing shows the function in action.
Note
The WDDXDeserialize
function depends on Xparse, an excellent, lightweight XML parser written entirely in Javascript. Xparse is made freely available by Jeremie for non-commercial use, and is found in the XPARSE.JS file, which is included on this book's CD for your convenience. Whenever you include wddxDes.js with the src
attribute of a SCRIPT
tag, you must also include XPARSE.JS in a separate SCRIPT
tag. If you do not, the WDDXDeserialize
function will not work correctly.
If you don't mind using some browser-dependant functionality, you could even consider allowing the user to save the contents of the Books recordset to a file on their local machine. This would allow people to save their work while they added and edited records, without affecting the database on the server until they are totally done with everything.
This type of approach might be ideal for situations where laptop users want to be able to bring a page up in their browser, then disconnect from the Internet to do their data-entry. They can save their work as they see fit, perhaps spending several hours or days offline as they enter all the information they need to. They are free to close their browser or turn off their computer. When they open up the page again, they just re-load the records from disk and continue working. When they are finished with all editing, they re-connect to the Internet and press the "Commit Changes To Server" button.
The code in Listing 23 provides this functionality for Internet Explorer 4.0 and up (see ). It gets the job done by adding two additional Javascript functions called LocalFileSave()
and LocalFileOpen()
. The functions are executed by the new "Save To File" and "Load from File" buttons, respectively. Both functions refer to the MyLocalFS
object which is defined by the OBJECT
tag that appears at the top of the template. The MyLocalFS
object is an instance of Microsoft's Scripting.FileSystemObject
COM object, which is available from Microsoft's website at the URL provided in the CODEBASE
attribute of the OBJECT
tag.
Tip
A complete discussion of the Scripting.FileSystemObject and the objects and methods it provides (like TextStream
, SaveTextFile
, ReadAll
, and Write
) is beyond the scope of this chapter. You can get more information about these methods in Microsoft's Internet Client SDK, which is highly recommended if you want to get deeper into Internet Explorer's more proprietary features. The SDK is available for purchase (cheap!) or free download from http://msdn.microsoft.com
The LocalFileSave()
function first uses Allaire's serialize
function to serialize the recordset into the WDDX Packet named BooksAsWDDX
(see the explanation for Listing 20 for details). It then uses Microsoft's CreateTextFile
function to create a file named BooksAsWDDX.txt
. The function returns a TextStream object which facilitates access to the new file. Next, the TextStream object's Write
method is used to "stream" the contents of the BooksAsWDDX
variable into the file. Then the TextStream object's Close
method is used to release the browser's "handle" on the file. Finally, the function displays a message on the browser's status bar so that that user can see how many records were actually saved to disk.
The LocalFileOpen()
function uses Microsoft's OpenTextFile
function to open the text file and associate a TextStream object with it, to facilitate reading from the file. The TextStream object's ReadAll
function is then used to load the file's entire contents into the BooksAsWDDX
variable. The browser's handle on the file is then released with the Close
method. Now the function needs to deserialize the WDDX packet back into the recordset of book information. To do this, it simply uses the WDDXDeserialize
function from Nate Weiss's wddxDes.js file. This returns the deserialized recordset, which immediately replaces the Books recordset that the rest of the functions interact with. Finally, the InitControls()
function is executed to "re-paint" the book titles in the SELECT
boxñwith the information that was loaded from disk.
Tip
For Netscape Communicator (version 4.0 and above), it should be possible to use Java, LiveConnect and "Signed Scripts" to get similar access to the browser's filesystem. Because this is somewhat more complicated, it is not covered in this chapter. To find information on getting this kind of access to the local filesystem with a Netscape browser, visit http://developer.netscape.com/
Listing 23 - JSBrowser4.cfm - This version allows the user to save and open packet files locally
<!--- Get data about books from database ---> <CFQUERY NAME="GetBooks" DATASOURCE="A2Z"> SELECT BookID, Title, ISBN, AuthorFirstName, AuthorLastName FROM Inventory ORDER BY Title </CFQUERY> <HTML><HEAD> <TITLE>Book Browser</TITLE> <!--- Include Microsoft's "Scripting Run-time Library" ---> <!--- Creates FileSystemObject object called MyLocalFS ---> <OBJECT ID="MyLocalFS" WIDTH=0 HEIGHT=0 CLASSID="CLSID:0D43FE01-F093-11CF-8940-00A0C9054228" CODEBASE="http://msdn.microsoft.com/scripting/scrrun/x86/srt31en.cab#version=3,1,0,2230"> </OBJECT> <!--- Include Allaire's WDDX / Javascript support ---> <SCRIPT src="/cfide/scripts/wddx.js" LANGUAGE="JAVASCRIPT"></SCRIPT> <!--- Include Nate Weiss's WDDXDeserializer function ---> <SCRIPT src="wddxDes.js" LANGUAGE="JAVASCRIPT"></SCRIPT> <!--- Include the Xparse XML Parser, required by wddxDes.js ---> <SCRIPT src="XParse.js" LANGUAGE="JAVASCRIPT"></SCRIPT> <SCRIPT LANGUAGE="JAVASCRIPT"> <!--- Convert query to Javascript object named "Books" ---> <CFWDDX ACTION="CFML2JS" INPUT="#GetBooks#" TOPLEVELVARIABLE="Books"> Books.addColumn("wasedited"); //////////////////////////////////////////////////// // This function fills the SELECT list with books function InitControls() { with (document.DataForm) { // Clear any current OPTIONS from the SELECT BookID.options.length = 0; // For each book record... for (var i = 1; i < Books.getRowCount(); i++) { // Create a new OPTION object NewOpt = new Option; NewOpt.value = Books.bookid[i]; NewOpt.text = Books.title[i]; // Add the new object to the SELECT list BookID.options[BookID.options.length] = NewOpt; } } } //////////////////////////////////////////////// // This function populates other INPUT elements // when an option in the SELECT box is clicked function FillControls() { with (document.DataForm) { // Add one to the OPTION number to get the data row number var RowNum = BookID.selectedIndex+1; // Populate textboxes with data in that row ISBN.value = Books.isbn[RowNum]; Title.value = Books.title[RowNum]; AuthorF.value = Books.authorfirstname[RowNum]; AuthorL.value = Books.authorlastname[RowNum]; } } //////////////////////////////////////////////// // This function "saves" data from the various // text boxes into the wddxRecordset object function KeepChanges() { with (document.DataForm) { // Add one to the OPTION number to get the data row number var SelectedBook = BookID.selectedIndex; var RowNum = SelectedBook + 1; // Populate javascript data array with info from textboxes Books.isbn[RowNum] = ISBN.value; Books.title[RowNum] = Title.value; Books.authorfirstname[RowNum] = AuthorF.value; Books.authorlastname[RowNum] = AuthorL.value; Books.wasedited[RowNum] = 'Yes'; // Re-initialize the SELECT list InitControls(); // Re-select the book that was selected before BookID.selectedIndex = -1; BookID.selectedIndex = SelectedBook; } } //////////////////////////////////////////////// // This function inserts a new row in the // wddxRecordset object, ready for editing function NewRecord() { with (document.DataForm) { // Add a new row to the recordset Books.addRows(1); NewRow = Books.getRowCount()-1; Books.setField(NewRow, "bookid", "new"); Books.setField(NewRow, "title", "(new)"); Books.setField(NewRow, "isbn", ""); Books.setField(NewRow, "authorfirstname", ""); Books.setField(NewRow, "authorlastname", ""); // Re-initialize the SELECT list InitControls(); // Re-select the book that was selected before BookID.selectedIndex = NewRow-1; FillControls(); } } //////////////////////////////////////////////// // This function inserts a new row in the // wddxRecordset object, ready for editing function CommitToServer() { with (document.DataForm) { // Create new WDDX Serializer object (supplied by Allaire) MySerializer = new WddxSerializer(); // Serialize the "Books" recordset into a WDDX packet BooksAsWDDX = MySerializer.serialize(Books); // Place the packet into the "WDDXContent" hidden field WDDXContent.value = BooksAsWDDX; // Submit the form submit(); } } //////////////////////////////////////////////// // This function saves the Books recordset to // disk as a WDDX Packet function LocalFileSave(){ // Serialize the Books recordset into WDDX Packet wddxSerializer = new WddxSerializer(); BooksAsWDDX = wddxSerializer.serialize(Books); // Save the WDDX Packet to disk as c:\BooksAsWDDX.txt textstream = MyLocalFS.CreateTextFile("c:\\BooksAsWDDX.txt", true); textstream.Write(BooksAsWDDX); textstream.Close(); // Show a message to the user window.defaultStatus = Books.getRowCount() + ' Records Saved'; } //////////////////////////////////////////////// // This function loads the Books recordset from // disk (as a WDDX Packet) and deserializes it function LocalFileOpen(){ // Load the WDDX Packet from BooksAsWDDX.txt file textstream = MyLocalFS.OpenTextFile("c:\\BooksAsWDDX.txt"); BooksAsWDDX = textstream.ReadAll(); textstream.Close() // Deserialize the packet into the Books recordset Books = WDDXDeserialize(BooksAsWDDX); InitControls(); // Show a message to the user window.defaultStatus = Books.getRowCount() + ' Records Loaded'; } </SCRIPT> </HEAD> <!--- After document loads, run InitControls() function ---> <BODY onLoad="InitControls();"> <H2>Offline Book Browser</H2> <FORM ACTION="JSBrowserCommit.cfm" METHOD="POST" NAME="DataForm"> <!--- CommitToServer() function gives this a value ---> <INPUT TYPE="HIDDEN" NAME="WDDXContent"> <TABLE BORDER CELLPADDING="10"> <TR VALIGN="TOP"> <TD> <!--- SELECT populated by InitControls() function ---> <!--- When clicked, calls FillControls() function ---> <SELECT NAME="BookID" SIZE="12" onChange="FillControls()"> <OPTION>============= (loading) ================= </SELECT> </TD> <TD> <!--- These controls get populated by FillControls() ---> <B>ISBN:</B><BR> <INPUT NAME="ISBN" SIZE="20" MAXLENGTH="13"><BR> <B>Author (first, last):</B><BR> <INPUT NAME="AuthorF" SIZE="18" MAXLENGTH="13"> <INPUT NAME="AuthorL" SIZE="20" MAXLENGTH="13"><BR> <B>Title:</B><BR> <INPUT NAME="Title" SIZE="40" MAXLENGTH="50"><BR> <P> <!--- Button to "keep" edits with KeepChanges() function ---> <INPUT TYPE="BUTTON" VALUE="Keep These Edits" onClick="KeepChanges()"> <!--- Button to cancel edits with FillChanges() function ---> <INPUT TYPE="BUTTON" VALUE="Cancel" onClick="FillControls()"> <!--- Button to insert new book with NewRecord() function ---> <INPUT TYPE="BUTTON" VALUE="New Record" onClick="NewRecord()"><BR> </TD> </TR> </TABLE> <!--- Button to save to server w/ CommitChanges() function ---> <P><CENTER> <INPUT TYPE="BUTTON" VALUE="Commit Changes To Server" onClick="CommitToServer()"><BR> <INPUT TYPE="BUTTON" VALUE="Save To File" onClick="LocalFileSave()"> <INPUT TYPE="BUTTON" VALUE="Load from File" onClick="LocalFileOpen()"><BR> </CENTER> </FORM> </BODY> </HTML>
Figure 15 - Once Javascript deserialization and file access is possible, pages can stand on their own
Note
This example will only work with Microsoft Internet Explorerñversion 4.0 and aboveñon Windows 95, Windows 98, and Windows NT. It requires that the Scripting.FileSystemObject
object be present on the user's machine. This COM object does not ship with Internet Explorer for security reasons, but may already be present on the user's machine anyway (for instance, if they have the Windows Scripting Host installed). At any rate, the OBJECT tag at the top of the template should automatically download and install the object if it is not already installed (prompting the user for permission before doing so, of course). For more information on Microsoft's FileSystemObject and the other objects in Microsoft's "Scripting Run-time Library", or to download the srt31en.cab file referred to in the OBJECT tag to your own webserver (so that the client machine goes to your webserver instead of Microsoft's webserver to install the object), visit http://msdn.microsoft.com/scripting/
The Book Browser examples that you've seen in this chapter all use form elements like SELECT
lists and TEXT
input boxes to allow the user to scroll through records and see correlated information about each record. If you wish, you can use Dynamic HTML (DHTML) and Cascading Style Sheets (CSS) to get a similar effect without using form controls. This can give your application a slicker, more visually sophisticated look without much additional coding.
Note
This section assumes that you have some familiarity with CSS and DTHML. A complete discussion of tags like STYLE
and DIV
ñand how to script them with innerText
, className
, and so onñis well beyond the scope of this chapter. Complete reference materials on Microsoft's implementation of CSS and DHTML is available in the Internet Client SDK. You can also refer to the HTML 4.0 specification, which is available for viewing at http://www.w3.org
For instance, the code in Listing 23 creates an interface somewhat like the interface shown back in Figure 12, except it's not editable and doesn't place any form elements on the page. The interface is shown in Figure 15. Instead of putting a SELECT
list on the page, this template uses a traditional CFOUTPUT
block to output a separate DIV
tag for each book's title along the left-hand side of the page. Because each DIV
tag has a CLASS="choice"
attribute, they inherit the font and color specifications that are defined for the choice class in the template's STYLE
block.
Each of the DIV tags contains an onMouseOver
and onMouseOut
event handler that changes the class of each DIV as the user "hovers" their mouse pointer over the titles, giving the page a slick, "interactive" feel. Each DIV tag also defines an onClick
handler which changes the tag's class to choiceSel
ñbold white text on a white backgroundñwhen the user clicks on a book title. The onClick
handler also fires the FillControls()
function, which changes the innerText
property of the related DIV tags to the appropriate Author name, Title, and so on.
Listing 24 - JSBrowserDHTML.cfm - Scripting with Dynamic HTML instead of form elements
<!--- Get data about books from database ---> <CFQUERY NAME="GetBooks" DATASOURCE="A2Z"> SELECT * FROM Inventory ORDER BY Title </CFQUERY> <HTML><HEAD> <TITLE>Book Browser</TITLE> <!--- Include Allaire's WDDX / Javascript support ---> <SCRIPT src="/cfide/scripts/wddx.js" LANGUAGE="JAVASCRIPT"></SCRIPT> <SCRIPT LANGUAGE="JAVASCRIPT"> <!--- Convert query to Javascript object named "Books" ---> <CFWDDX ACTION="CFML2JS" INPUT="#GetBooks#" TOPLEVELVARIABLE="Books"> //////////////////////////////////////////////// // This function populates other INPUT elements // when an option in the SELECT box is clicked function FillControls(RowNum) { with (document.DataForm) { RowNum = RowNum - 1; // Populate textboxes with data in that row ISBN.innerText = Books.isbn[RowNum]; Title.innerText = Books.title[RowNum]; Pages.innerText = Books.pages[RowNum]; Description.innerText = Books.description[RowNum]; Author.innerText = Books.authorlastname[RowNum] + ', ' + Books.authorfirstname[RowNum]; } } </SCRIPT> </HEAD> <BODY> <!--- Define CSS styles to control the "look" of the DIV tags---> <STYLE TYPE="text/css"> Div {width:350px; padding-left:3px; font:smaller Arial} Div.choice {background:wheat}; Div.choiceSel {background:black; color:white; font-weight:bold}; Div.choiceOver {background:beige; color:brown}; Div.bar {background:brown; color:white; font:bold small Arial}; H2 {background:black; color:white} </STYLE> <H2>Offline Book Browser</H2> <FORM NAME="DataForm"> <TABLE CELLPADDING="10"> <TR VALIGN="TOP"> <!--- Books to choose from. User can click on each Title ---> <TD> <DIV CLASS="bar">Books</DIV> <CFOUTPUT QUERY="GetBooks"> <DIV CLASS="choice" onClick="this.className = 'choiceSel'; FillControls(#CurrentRow#)" onMouseOver="this.className = 'choiceOver'" onMouseOut="this.className = 'choice'">#Title#</DIV> </CFOUTPUT> </TD> <!--- This info gets updated when user clicks on a Title ---> <TD> <DIV CLASS="bar">ISBN</DIV> <DIV CLASS="choice" ID="ISBN"></DIV><BR> <DIV CLASS="bar">Author</DIV> <DIV CLASS="choice" ID="Author"></DIV><BR> <DIV CLASS="bar">Title</DIV> <DIV CLASS="choice" ID="Title"></DIV><BR> <DIV CLASS="bar">Pages</DIV> <DIV CLASS="choice" ID="Pages"></DIV><BR> </TD> </TR> <!--- So does the description, underneath ---> <TR><TD COLSPAN="2"> <DIV CLASS="bar">Description</DIV> <DIV CLASS="choice" ID="Description" STYLE="height:100px;width=auto"> </DIV><BR> </TD></TR> </TABLE> </FORM> </BODY> </HTML>
Figure 16 - JSBrowser3.cfm - This version of the Book Browser adds DHTML and CSS formatting for a sophisticated look